﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Xml;
using System.Web;

namespace SPBert.SharePoint_BlogML
{
    class Program
    {
        #region Structure for holding a pending site conversion
        struct SiteToConvert
        {
            public string url;
            public string username;
            public string password;
            public string domain;
            public string outputFile;
            public string title;
            public string subtitle;
            public string authorEmail;
            public string authorName;
            public string authorUsername;
            public string attachmentsPath;
            public string errorFile;
            public string redirectFile;
            public DateTime dateCreated;
            public DateTime dateModified;
        }
        #endregion Structure for holding a pending site conversion

        static void Main(string[] args)
        {
            #region Process input parameters
            List<SiteToConvert> sitesToConvert = new List<SiteToConvert>();
            SiteToConvert baseSiteToConvert = new SiteToConvert();

            string lastArg = "";
            string inputFile = "";
            foreach (string arg in args)
            {
                switch (lastArg)
                {
                    case "-attachmentspath":
                        baseSiteToConvert.attachmentsPath = arg;
                        break;
                    case "-authoremail":
                        baseSiteToConvert.authorEmail = arg;
                        break;
                    case "-author":
                    case "-authorname":
                        baseSiteToConvert.authorName = arg;
                        break;
                    case "-authorusername":
                        baseSiteToConvert.authorUsername = arg;
                        break;
                    case "-created":
                    case "-datecreated":
                        DateTime.TryParse(arg, out baseSiteToConvert.dateCreated);
                        break;
                    case "-domain":
                        baseSiteToConvert.domain = arg;
                        break;
                    case "-modified":
                    case "-datemodified":
                        DateTime.TryParse(arg, out baseSiteToConvert.dateModified);
                        break;
                    case "-outputfile":
                        baseSiteToConvert.outputFile = arg;
                        break;
                    case "-password":
                        baseSiteToConvert.password = arg;
                        break;
                    case "-subtitle":
                        baseSiteToConvert.subtitle = arg;
                        break;
                    case "-title":
                        baseSiteToConvert.title = arg;
                        break;
                    case "-url":
                        baseSiteToConvert.url = arg;
                        break;
                    case "-username":
                        baseSiteToConvert.username = arg;
                        break;
                    case "-input":
                    case "-inputFile":
                        inputFile = arg;
                        break;
                    case "-error":
                    case "-errorFile":
                        baseSiteToConvert.errorFile = arg;
                        break;
                    case "-redirect":
                    case "-redirectFile":
                        baseSiteToConvert.redirectFile = arg;
                        break;
                }
                lastArg = arg.ToLower();
            }
            #endregion Process input parameters

            #region Add the single input site to our conversion queue
            if (!string.IsNullOrEmpty(baseSiteToConvert.url) && !string.IsNullOrEmpty(baseSiteToConvert.username) && !string.IsNullOrEmpty(baseSiteToConvert.password) && !string.IsNullOrEmpty(baseSiteToConvert.domain))
            {
                if (string.IsNullOrEmpty(baseSiteToConvert.outputFile))
                    baseSiteToConvert.outputFile = GetOutputFileName(baseSiteToConvert.url);
                sitesToConvert.Add(baseSiteToConvert);
            }
            #endregion Add the single input site to our conversion queue

            if (sitesToConvert.Count < 1 && string.IsNullOrEmpty(inputFile))
                inputFile = "Input.xml";

            if (!string.IsNullOrEmpty(inputFile))
            {
                #region Process an XML file containing site definitions
                if (File.Exists(inputFile))
                {
                    StreamReader sr = new StreamReader(inputFile);
                    string inputFileContents = sr.ReadToEnd();
                    sr.Dispose();

                    #region Process global defaults for all site definitions
                    string globalSettingsXML = GetStringBetween(inputFileContents, "<Global>", "</Global>");
                    string globalAttachmentsPath = GetStringBetween(globalSettingsXML, "<AttachmentsPath>", "</AttachmentsPath>");
                    string globalAuthorEmail = GetStringBetween(globalSettingsXML, "<AuthorEmail>", "</AuthorEmail>");
                    string globalAuthorName = GetStringBetween(globalSettingsXML, "<Author>", "</Author>");
                    if (string.IsNullOrEmpty(globalAuthorName))
                        globalAuthorName = GetStringBetween(globalSettingsXML, "<AuthorName>", "</AuthorName>");
                    string globalAuthorUsername = GetStringBetween(globalSettingsXML, "<AuthorUsername>", "</AuthorUsername>");
                    DateTime globalDateCreated = new DateTime();
                    DateTime.TryParse(GetStringBetween(globalSettingsXML, "<DateCreated>", "</DateCreated>"), out globalDateCreated);
                    DateTime globalDateModified = new DateTime();
                    DateTime.TryParse(GetStringBetween(globalSettingsXML, "<DateModified>", "</DateModified>"), out globalDateModified);
                    string globalDomain = GetStringBetween(globalSettingsXML, "<Domain>", "</Domain>");
                    string globalErrorFile = GetStringBetween(globalSettingsXML, "<Error>", "</Error>");
                    if (string.IsNullOrEmpty(globalErrorFile))
                        globalErrorFile = GetStringBetween(globalSettingsXML, "<ErrorFile>", "</ErrorFile>");
                    string globalOutputFile = GetStringBetween(globalSettingsXML, "<Output>", "</Output>");
                    if (string.IsNullOrEmpty(globalOutputFile))
                        globalOutputFile = GetStringBetween(globalSettingsXML, "<OutputFile>", "</OutputFile>");
                    string globalPassword = GetStringBetween(globalSettingsXML, "<Password>", "</Password>");
                    string globalRedirectFile = GetStringBetween(globalSettingsXML, "<Redirect>", "</Redirect>");
                    if (string.IsNullOrEmpty(globalRedirectFile))
                        globalRedirectFile = GetStringBetween(globalSettingsXML, "<RedirectFile>", "</RedirectFile>");
                    string globalSubtitle = GetStringBetween(globalSettingsXML, "<SubTitle>", "</SubTitle>");
                    string globalTitle = GetStringBetween(globalSettingsXML, "<Title>", "</Title>");
                    string globalUrl = GetStringBetween(globalSettingsXML, "<URL>", "</URL>");
                    if (string.IsNullOrEmpty(globalOutputFile))
                        globalOutputFile = GetOutputFileName(globalUrl);
                    string globalUsername = GetStringBetween(globalSettingsXML, "<Username>", "</Username>");
                    #endregion Process global defaults for all site definitions

                    string inputFileLower = inputFileContents.ToLower();
                    int pos = 0;
                    while (pos > -1)
                    {
                        pos = inputFileLower.IndexOf("<site>", pos);
                        if (pos > -1)
                        {
                            int pos2 = inputFileLower.IndexOf("</site>", pos);
                            if (pos2 > 0)
                            {
                                string siteXML = inputFileContents.Substring(pos + 6, pos2 - pos - 6);

                                SiteToConvert currentSiteToConvert = new SiteToConvert();
                                currentSiteToConvert.attachmentsPath = GetStringBetween(siteXML, "<AttachmentsPath>", "</AttachmentsPath>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.attachmentsPath))
                                    currentSiteToConvert.attachmentsPath = globalAttachmentsPath;
                                currentSiteToConvert.authorEmail = GetStringBetween(siteXML, "<AuthorEmail>", "</AuthorEmail>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.authorEmail))
                                    currentSiteToConvert.authorEmail = globalAuthorEmail;
                                currentSiteToConvert.authorName = GetStringBetween(siteXML, "<Author>", "</Author>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.authorName))
                                    currentSiteToConvert.authorName = GetStringBetween(siteXML, "<AuthorName>", "</AuthorName>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.authorName))
                                    currentSiteToConvert.authorName = globalAuthorName;
                                currentSiteToConvert.authorUsername = GetStringBetween(siteXML, "<AuthorUsername>", "</AuthorUsername>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.authorUsername))
                                    currentSiteToConvert.authorUsername = globalAuthorUsername;
                                DateTime.TryParse(GetStringBetween(siteXML, "<DateCreated>", "</DateCreated>"), out currentSiteToConvert.dateCreated);
                                if (currentSiteToConvert.dateCreated.Year < 1900)
                                    currentSiteToConvert.dateCreated = globalDateCreated;
                                DateTime.TryParse(GetStringBetween(siteXML, "<DateModified>", "</DateModified>"), out currentSiteToConvert.dateModified);
                                if (currentSiteToConvert.dateModified.Year < 1900)
                                    currentSiteToConvert.dateModified = globalDateModified;
                                currentSiteToConvert.domain = GetStringBetween(siteXML, "<Domain>", "</Domain>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.domain))
                                    currentSiteToConvert.domain = globalDomain;
                                currentSiteToConvert.errorFile = GetStringBetween(siteXML, "<Error>", "</Error>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.errorFile))
                                    currentSiteToConvert.errorFile = GetStringBetween(siteXML, "<ErrorFile>", "</ErrorFile>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.errorFile))
                                    currentSiteToConvert.errorFile = globalErrorFile;
                                currentSiteToConvert.outputFile = GetStringBetween(siteXML, "<Output>", "</Output>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.outputFile))
                                    currentSiteToConvert.outputFile = GetStringBetween(siteXML, "<OutputFile>", "</OutputFile>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.outputFile))
                                    currentSiteToConvert.outputFile = globalOutputFile;
                                currentSiteToConvert.password = GetStringBetween(siteXML, "<Password>", "</Password>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.password))
                                    currentSiteToConvert.password = globalPassword;
                                currentSiteToConvert.redirectFile = GetStringBetween(siteXML, "<Redirect>", "</Redirect>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.redirectFile))
                                    currentSiteToConvert.redirectFile = GetStringBetween(siteXML, "<RedirectFile>", "</RedirectFile>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.redirectFile))
                                    currentSiteToConvert.redirectFile = globalRedirectFile;
                                currentSiteToConvert.subtitle = GetStringBetween(siteXML, "<SubTitle>", "</SubTitle>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.subtitle))
                                    currentSiteToConvert.subtitle = globalSubtitle;
                                currentSiteToConvert.title = GetStringBetween(siteXML, "<Title>", "</Title>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.title))
                                    currentSiteToConvert.title = globalTitle;
                                currentSiteToConvert.url = GetStringBetween(siteXML, "<URL>", "</URL>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.url))
                                    currentSiteToConvert.url = globalUrl;
                                if (string.IsNullOrEmpty(currentSiteToConvert.outputFile))
                                    currentSiteToConvert.outputFile = GetOutputFileName(currentSiteToConvert.url);
                                currentSiteToConvert.username = GetStringBetween(siteXML, "<Username>", "</Username>");
                                if (string.IsNullOrEmpty(currentSiteToConvert.username))
                                    currentSiteToConvert.username = globalUsername;

                                sitesToConvert.Add(currentSiteToConvert);
                            }
                            pos = pos2 + 7;
                        }
                    }
                }
                else
                    Console.WriteLine("File '" + inputFile + "' not found.");
                #endregion Process an XML file containing site definitions
            }

            if (sitesToConvert.Count < 1)
                Console.WriteLine("Please specify parameters either using -inputFile or a combination of -URL, -userName, -password, and -domain.");

            // Used for mapping old URLS to their new counterparts
            Dictionary<string, string> urlsToRedirect = new Dictionary<string, string>();

            #region Request blog posts, categories, comments, and attachments
            foreach (SiteToConvert siteToConvert in sitesToConvert)
            {
                #region Update our lists proxy endpoint
                ListsProxy.Lists listsProxy = new ListsProxy.Lists();
                listsProxy.Url = (siteToConvert.url + "/_vti_bin/Lists.asmx");
                listsProxy.Credentials = new NetworkCredential(siteToConvert.username, siteToConvert.password, siteToConvert.domain);
                #endregion Update our lists proxy endpoint

                XmlDocument doc = new XmlDocument();
                XmlNode query = doc.CreateNode(XmlNodeType.Element, "Query", "");
                XmlNode viewFields = doc.CreateNode(XmlNodeType.Element, "ViewFields", "");

                #region Check if file exists so we know whether to append to it
                bool fileExists = false;
                if (File.Exists(siteToConvert.outputFile))
                {
                    FileInfo fi = new FileInfo(siteToConvert.outputFile);
                    if (fi.Length > 0)
                        fileExists = true;
                }
                #endregion Check if file exists so we know whether to append to it

                // Track errors
                using (StreamWriter errorStream = new StreamWriter(siteToConvert.errorFile, true))
                {
                    using (StreamWriter outputStream = new StreamWriter(siteToConvert.outputFile, true))
                    {
                        string domain = siteToConvert.url.Substring(0, siteToConvert.url.IndexOf("/", 8));
                        string relativePath = siteToConvert.url.Substring(siteToConvert.url.IndexOf("/", 8));

                        if (!fileExists)
                            outputStream.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
                        outputStream.WriteLine("<blog date-created=\"" + siteToConvert.dateCreated.ToString("s") + "\" xmlns=\"http://www.blogml.com/2006/09/BlogML\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">");
                        outputStream.WriteLine("\t<title type=\"text\">" + XMLEscape(siteToConvert.title) + "</title>");
                        outputStream.WriteLine("\t<sub-title type=\"text\">" + XMLEscape(siteToConvert.subtitle) + "</sub-title>");
                        outputStream.WriteLine("\t<authors>");
                        outputStream.WriteLine("\t\t<author id=\"" + siteToConvert.authorUsername + "\" date-created=\"" + siteToConvert.dateCreated.ToString("s") + "\" date-modified=\"" + siteToConvert.dateModified.ToString("s") + "\" approved=\"true\" email=\"" + siteToConvert.authorEmail + "\">");
                        outputStream.WriteLine("\t\t\t<title type=\"text\">" + XMLEscape(siteToConvert.authorName) + "</title>");
                        outputStream.WriteLine("\t\t</author>");
                        outputStream.WriteLine("\t</authors>");
                        outputStream.WriteLine("\t<extended-properties>");
                        outputStream.WriteLine("\t\t<property name=\"CommentModeration\" value=\"Anonymous\" />");
                        outputStream.WriteLine("\t\t<property name=\"SendTrackback\" value=\"Yes\" />");
                        outputStream.WriteLine("\t</extended-properties>");
                        outputStream.WriteLine("\t<categories>");

                        #region Request all categories
                        XmlNode listItems = listsProxy.GetListItems("Categories", null, query, viewFields, "2000", null, null);
                        XmlNamespaceManager namespaceManager = new XmlNamespaceManager(listItems.OwnerDocument.NameTable);
                        namespaceManager.AddNamespace("z", "#RowsetSchema");
                        namespaceManager.AddNamespace("sp", "http://schemas.microsoft.com/sharepoint/soap/");
                        namespaceManager.AddNamespace("s", "uuid:BDC6E3F0-6DA3-11d1-A2A3-0AA00C14882");
                        namespaceManager.AddNamespace("dt", "uuid:C2F41010-65B3-11d1-A29F-00AA00C14882");
                        namespaceManager.AddNamespace("rs", "urn:schemas-microsoft-com:rowset");
                        XmlNodeList categoryNodes = listItems.SelectNodes("rs:data/z:row", namespaceManager);
                        #endregion Request all categories

                        #region Process categories
                        Dictionary<string, string> categoryMappings = new Dictionary<string, string>();
                        foreach (XmlNode categoryNode in categoryNodes)
                        {
                            string categoryID = categoryNode.Attributes["ows_ID"].Value;
                            string categoryDateCreated = DateTime.Parse(categoryNode.Attributes["ows_Created"].Value).ToString("s");
                            string categoryDateModified = DateTime.Parse(categoryNode.Attributes["ows_Modified"].Value).ToString("s");
                            string categoryTitle = categoryNode.Attributes["ows_Title"].Value;

                            outputStream.WriteLine("\t\t<category id=\"" + categoryTitle + "\" date-created=\"" + categoryDateCreated + "\" date-modified=\"" + categoryDateModified + "\" approved=\"true\" description=\"" + categoryTitle + "\" parentref=\"0\">");
                            outputStream.WriteLine("\t\t\t<title type=\"text\">" + XMLEscape(categoryTitle) + "</title>");
                            outputStream.WriteLine("\t\t</category>");

                            categoryMappings.Add(categoryTitle, categoryID);
                        }
                        #endregion Process categories

                        outputStream.WriteLine("\t</categories>");
                        outputStream.WriteLine("\t<posts>");

                        #region Request all posts
                        listItems = listsProxy.GetListItems("Posts", null, query, viewFields, "2000", null, null);
                        XmlNodeList postNodes = listItems.SelectNodes("rs:data/z:row", namespaceManager);
                        #endregion Request all posts

                        #region Process posts
                        foreach (XmlNode postNode in postNodes)
                        {
                            string postID = postNode.Attributes["ows_ID"].Value;
                            DateTime postDateCreated = DateTime.Parse(postNode.Attributes["ows_Created"].Value);
                            DateTime postDateModified = DateTime.Parse(postNode.Attributes["ows_Modified"].Value);
                            string postURL = postNode.Attributes["ows_ServerUrl"].Value.Replace("_.000", "");
                            string postTitle = postNode.Attributes["ows_Title"].Value;
                            string postBody = postNode.Attributes["ows_Body"].Value;
                            string postCategoryString = postNode.Attributes["ows_PostCategory"].Value;

                            outputStream.WriteLine("\t\t<post id=\"" + postID + "\" date-created=\"" + postDateCreated.ToString("s") + "\" date-modified=\"" + postDateModified.ToString("s") + "\" approved=\"true\" post-url=\"" + postURL + "\" type=\"normal\" hasexcerpt=\"false\" views=\"0\">");
                            outputStream.WriteLine("\t\t\t<title type=\"text\">" + XMLEscape(postTitle) + "</title>");
                            outputStream.WriteLine("\t\t\t<content type=\"text\">" + XMLEscape(postBody) + "</content>");
                            outputStream.WriteLine("\t\t\t<post-name type=\"text\">" + XMLEscape(postTitle) + "</post-name>");
                            outputStream.WriteLine("\t\t\t<categories>");

                            string[] postCategories = postCategoryString.Split(';');
                            for (int i = 1; i < postCategories.Length; i += 2)
                                outputStream.WriteLine("\t\t\t\t<category ref=\"" + postCategories[i].Substring(1) + "\" />");
                            outputStream.WriteLine("\t\t\t</categories>");
                            outputStream.WriteLine("\t\t\t<comments>");

                            #region Request all comments
                            query.InnerXml = "<Where><Eq><FieldRef Name='PostTitle' LookupId='True' /><Value Type='Lookup'>" + postID + "</Value></Eq></Where>";
                            listItems = listsProxy.GetListItems("Comments", null, query, viewFields, "2000", null, null);
                            XmlNodeList commentNodes = listItems.SelectNodes("rs:data/z:row", namespaceManager);
                            #endregion Request all comments

                            #region Process comments
                            foreach (XmlNode commentNode in commentNodes)
                            {
                                string commentID = commentNode.Attributes["ows_ID"].Value;
                                string commentDateCreated = DateTime.Parse(commentNode.Attributes["ows_Created"].Value).ToString("s");
                                string commentDateModified = DateTime.Parse(commentNode.Attributes["ows_Modified"].Value).ToString("s");
                                string commenterName = "";
                                if (commentNode.Attributes["ows_CommenterName"] != null)
                                    commenterName = commentNode.Attributes["ows_CommenterName"].Value;
                                string commenterEmail = "";
                                if (commentNode.Attributes["ows_CommenterEmail"] != null)
                                    commenterEmail = commentNode.Attributes["ows_CommenterEmail"].Value;
                                string commentTitle = "";
                                if (commentNode.Attributes["ows_Title"] != null)
                                    commentTitle = commentNode.Attributes["ows_Title"].Value;
                                string commentBody = "";
                                if (commentNode.Attributes["ows_Body"] != null)
                                    commentBody = commentNode.Attributes["ows_Body"].Value;

                                outputStream.WriteLine("\t\t\t\t<comment id=\"" + commentID + "\" date-created=\"" + commentDateCreated + "\" date-modified=\"" + commentDateModified + "\" approved=\"true\" user-name=\"" + XMLEscape(commenterName) + "\" user-url=\"" + XMLEscape(commenterEmail) + "\">");
                                outputStream.WriteLine("\t\t\t\t\t<title type=\"text\">" + XMLEscape(commentTitle) + "</title>");
                                outputStream.WriteLine("\t\t\t\t\t<content type=\"text\">" + XMLEscape(commentBody) + "</content>");
                                outputStream.WriteLine("\t\t\t\t</comment>");
                            }
                            #endregion Process comments

                            outputStream.WriteLine("\t\t\t</comments>");
                            outputStream.WriteLine("\t\t\t<authors>");
                            outputStream.WriteLine("\t\t\t\t<author ref=\"" + siteToConvert.authorName + "\" />");
                            outputStream.WriteLine("\t\t\t</authors>");
                            outputStream.WriteLine("\t\t</post>");

                            #region Track URLs to remap
                            urlsToRedirect[relativePath + "Pages/Post.aspx?_ID=" + postID] = "/" + siteToConvert.authorUsername + "/" + postDateCreated.Year + "/" + PadNumber(postDateCreated.Month.ToString()) + "/" + CleanFileName(postTitle, false) + "/";
                            urlsToRedirect[postURL] = "/" + siteToConvert.authorUsername + "/" + postDateCreated.Year + "/" + PadNumber(postDateCreated.Month.ToString()) + "/" + CleanFileName(postTitle, false) + "/";
                            #endregion Track URLs to remap

                            #region Request and process attachments
                            using (WebClient wc = new WebClient())
                            {
                                XmlNode attachments = listsProxy.GetAttachmentCollection("Posts", postID);
                                foreach (XmlNode attachment in attachments)
                                {
                                    string fileURL = attachment.InnerXml;
                                    string fileName = siteToConvert.authorUsername + "-" + fileURL.Substring(fileURL.LastIndexOf("/") + 1).ToLower();
                                    string localPath = siteToConvert.attachmentsPath + fileURL.Replace("/", "\\");
                                    try
                                    {
                                        wc.DownloadFile(fileURL, siteToConvert.attachmentsPath + "\\" + CleanFileName(fileName, true));
                                    }
                                    catch (Exception)
                                    {
                                        errorStream.WriteLine(fileURL);
                                    }

                                    DateTime fileDate = postDateCreated;
                                    #region Track URLs to remap
                                    urlsToRedirect[fileURL.Replace(domain, "")] = "/wp-content/uploads/" + DateTime.Now.Year.ToString() + "/" + PadNumber(DateTime.Now.Month.ToString()) + "/" + CleanFileName(fileName, true);
                                    if (fileURL.Contains(" "))
                                        urlsToRedirect[fileURL.Replace(domain, "").Replace(" ", "%20")] = "/wp-content/uploads/" + DateTime.Now.Year.ToString() + "/" + PadNumber(DateTime.Now.Month.ToString()) + "/" + CleanFileName(fileName, true);
                                    #endregion Track URLs to remap
                                }
                            }
                            #endregion Request and process attachments
                        }
                        #endregion Process posts

                        outputStream.WriteLine("\t</posts>");
                        outputStream.WriteLine("</blog>");

                        #region Track URLs to remap
                        urlsToRedirect[relativePath.Substring(0, relativePath.Length - 1)] = "/" + siteToConvert.authorUsername + "/";
                        urlsToRedirect[relativePath] = "/" + siteToConvert.authorUsername + "/";
                        urlsToRedirect[relativePath + "Default.aspx"] = "/" + siteToConvert.authorUsername + "/";
                        urlsToRedirect[relativePath + "Pages"] = "/" + siteToConvert.authorUsername + "/";
                        urlsToRedirect[relativePath + "Pages/"] = "/" + siteToConvert.authorUsername + "/";
                        urlsToRedirect[relativePath + "Pages/Default.aspx"] = "/" + siteToConvert.authorUsername + "/";
                        urlsToRedirect[relativePath + "Pages/AllPosts.aspx"] = "/" + siteToConvert.authorUsername + "/";
                        #endregion Track URLs to remap

                        #region Request and process other assets
                        listItems = listsProxy.GetListCollection();
                        XmlNodeList listNameNodes = listItems.SelectNodes("//sp:List", namespaceManager);

                        using (WebClient wc = new WebClient())
                        {
                            foreach (XmlNode listNameNode in listNameNodes)
                            {
                                // Redirect the site's RSS feed
                                if (listNameNode.Attributes["Title"].Value == "Posts")
                                    urlsToRedirect[relativePath + "_layouts/listfeed.aspx?List=" + listNameNode.Attributes["ID"].Value] = "/author/" + siteToConvert.authorUsername + "/feed/";

                                // Download content types that derive from document only
                                if (listNameNode.Attributes["ServerTemplate"].Value.StartsWith("10") && listNameNode.Attributes["Title"].Value != "Links")
                                {
                                    query.InnerXml = "";
                                    listItems = listsProxy.GetListItems(listNameNode.Attributes["Title"].Value, null, query, viewFields, "2000", null, null);
                                    XmlNodeList listItemNodes = listItems.SelectNodes("rs:data/z:row", namespaceManager);
                                    foreach (XmlNode listItemNode in listItemNodes)
                                    {
                                        string fileURL = listItemNode.Attributes["ows_ServerUrl"].Value.Replace("_.000", "");
                                        string fileName = siteToConvert.authorUsername + "-" + fileURL.Substring(fileURL.LastIndexOf("/") + 1).ToLower();
                                        string localPath = siteToConvert.attachmentsPath + fileURL.Replace("/", "\\");
                                        try
                                        {
                                            wc.DownloadFile(domain + fileURL, siteToConvert.attachmentsPath + "\\" + CleanFileName(fileName, true));
                                        }
                                        catch (Exception)
                                        {
                                            errorStream.WriteLine(domain + fileURL);
                                        }

                                        DateTime fileDate = DateTime.Parse(listItemNode.Attributes["ows_Created"].Value);
                                        #region Track URLs to remap
                                        urlsToRedirect[fileURL] = "/wp-content/uploads/" + DateTime.Now.Year.ToString() + "/" + PadNumber(DateTime.Now.Month.ToString()) + "/" + CleanFileName(fileName, true);
                                        if (fileURL.Contains(" "))
                                            urlsToRedirect[fileURL.Replace(" ", "%20")] = "/wp-content/uploads/" + DateTime.Now.Year.ToString() + "/" + PadNumber(DateTime.Now.Month.ToString()) + "/" + CleanFileName(fileName, true);
                                        #endregion Track URLs to remap
                                    }
                                }
                            }
                        }
                        #endregion Request and process other assets
                    }
                }
            }
            #endregion Request blog posts, categories, comments, and attachments

            #region Write URLs to remap
            using (StreamWriter redirectStream = new StreamWriter(sitesToConvert[0].redirectFile, true))
            {
                urlsToRedirect.OrderBy(u => u.Key);
                foreach (string url in urlsToRedirect.Keys)
                    redirectStream.Write("INSERT INTO wp_redirection_items (url,action_type,regex,position,match_type,action_data,action_code,last_access,group_id) VALUES ('" + url.Replace("'", "''") + "','url','0','0','url','" + urlsToRedirect[url].Replace("'", "''") + "','301','0000-00-00 00:00:00','1');\r\n");
            }
            #endregion Write URLs to remap

            #region Rewrite links to use remapped URLs
            foreach (SiteToConvert siteToConvert in sitesToConvert)
            {
                string contents = "";
                using (StreamReader sr = new StreamReader(siteToConvert.outputFile))
                {
                    contents = sr.ReadToEnd();
                    while (contents.IndexOf("//lists", StringComparison.CurrentCultureIgnoreCase) > -1)
                        contents = contents.Replace("//Lists", "/Lists").Replace("//lists", "/lists");
                }
                string contentsUpper = contents.ToUpper();

                // Sort URLs to redirect by length, descending
                IEnumerable<string> urlsToRedirectKeys = from s in urlsToRedirect orderby s.Value.Length descending select s.Key;

                using (StreamWriter outputStream = new StreamWriter(siteToConvert.outputFile, false))
                {
                    int contentsPos = 0;
                    while (contentsPos > -1)
                    {
                        int lastContentsPos = contentsPos;
                        contentsPos = contentsUpper.IndexOf("<CONTENT TYPE=\"TEXT\">", contentsPos);
                        if (contentsPos > -1)
                        {
                            outputStream.Write(contents.Substring(lastContentsPos, contentsPos + 21 - lastContentsPos));
                            outputStream.Flush();
                            int contentsEndPos = contentsUpper.IndexOf("</CONTENT>", contentsPos);

                            string subContents = contents.Substring(contentsPos + 21, contentsEndPos - contentsPos - 21);
                            string subContentsUpper = subContents.ToUpper();
                            foreach (string url in urlsToRedirectKeys)
                            {
                                if (subContentsUpper.IndexOf(url.ToUpper()) > -1)
                                {
                                    StringBuilder sb = new StringBuilder();
                                    int urlPos = 0;
                                    while (urlPos > -1)
                                    {
                                        int lastUrlPos = urlPos;
                                        urlPos = subContentsUpper.IndexOf(url.ToUpper(), urlPos);
                                        if (urlPos > -1)
                                        {
                                            sb.Append(subContents.Substring(lastUrlPos, urlPos - lastUrlPos));
                                            sb.Append(urlsToRedirect[url]);
                                            urlPos += url.Length;
                                        }
                                        else
                                            sb.Append(subContents.Substring(lastUrlPos));
                                    }
                                    subContents = sb.ToString();
                                    subContentsUpper = subContents.ToUpper();
                                }
                            }

                            outputStream.Write(subContents);
                            contentsPos = contentsEndPos;
                        }
                        else
                            outputStream.Write(contents.Substring(lastContentsPos));
                    }
                }
            }
            #endregion Rewrite links to use remapped URLs
        }

        // Provide SEO-friendly file name
        private static string CleanFileName(string input, bool allowPeriods)
        {
            int period = 46;
            if (!allowPeriods)
                period = -1;

            StringBuilder sb = new StringBuilder();
            int lastOrd = -1;
            for (int i = 0; i < input.Length; i++)
            {
                int ord = (int)input[i];
                if ((ord >= 48 && ord <= 57) || (ord >= 65 && ord <= 90) || (ord >= 97 && ord <= 122) || ord == period)
                    sb.Append(input[i]);
                else
                {
                    if (((lastOrd >= 48 && lastOrd <= 57) || (lastOrd >= 65 && lastOrd <= 90) || (lastOrd >= 97 && lastOrd <= 122) || lastOrd == 39) && ord != 39)
                        sb.Append("-");
                }

                lastOrd = ord;
            }

            string output = sb.ToString();
            while (output.EndsWith("-") || output.EndsWith("."))
                output = output.Substring(0, output.Length - 1);

            return output;
        }

        // Calculate local output filename
        private static string GetOutputFileName(string url)
        {
            if (url.EndsWith("/"))
                url = url.Substring(0, url.Length - 1);
            return HttpUtility.UrlEncode(url.Replace("http://", "")) + ".xml";
        }

        // Extracting a substring
        private static string GetStringBetween(string haystack, string needle1, string needle2)
        {
            string haystackLower = haystack.ToLower();
            int pos1 = haystackLower.IndexOf(needle1.ToLower());
            if (pos1 > -1)
            {
                int pos2 = haystackLower.IndexOf(needle2.ToLower(), pos1 + needle1.Length);
                if (pos2 > -1)
                    return haystack.Substring(pos1 + needle1.Length, pos2 - pos1 - needle1.Length);
            }
            return "";
        }

        // Escape invalid XML characters
        private static string XMLEscape(string input)
        {
            // Ensure Unicode encoding
            UnicodeEncoding uni = new UnicodeEncoding();
            return System.Security.SecurityElement.Escape(uni.GetString(uni.GetBytes(input)).Replace("“", "").Replace("”", ""));
        }

        // Ensure numbers are at least two digits
        private static string PadNumber(string input)
        {
            if (input.Length < 2)
                return "0" + input;
            else
                return input;
        }
    }
}
